﻿namespace FastColoredTextBoxNS
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Text.RegularExpressions;
    using System.Windows.Forms;

    /// <summary>
    /// 	Popup menu for autocomplete
    /// </summary>
    [Browsable(false)]
    public class AutocompleteMenu : ToolStripDropDown
    {
        private readonly AutocompleteListView listView;

        public ToolStripControlHost host;

        public Range Fragment { get; internal set; }

        /// <summary>
        /// 	Regex pattern for serach fragment around caret
        /// </summary>
        public string SearchPattern { get; set; }

        /// <summary>
        /// 	Minimum fragment length for popup
        /// </summary>
        public int MinFragmentLength { get; set; }

        /// <summary>
        /// 	User selects item
        /// </summary>
        public event EventHandler<SelectingEventArgs> Selecting;

        /// <summary>
        /// 	It fires after item inserting
        /// </summary>
        public event EventHandler<SelectedEventArgs> Selected;

        /// <summary>
        /// 	Occurs when popup menu is opening
        /// </summary>
        public new event EventHandler<CancelEventArgs> Opening;

        /// <summary>
        /// 	Allow TAB for select menu item
        /// </summary>
        public bool AllowTabKey
        {
            get
            {
                return this.listView.AllowTabKey;
            }
            set
            {
                this.listView.AllowTabKey = value;
            }
        }

        /// <summary>
        /// 	Interval of menu appear (ms)
        /// </summary>
        public int AppearInterval
        {
            get
            {
                return this.listView.AppearInterval;
            }
            set
            {
                this.listView.AppearInterval = value;
            }
        }

        /// <summary>
        /// 	Back color of selected item
        /// </summary>
        [DefaultValue(typeof(Color), "Orange")]
        public Color SelectedColor
        {
            get
            {
                return this.listView.SelectedColor;
            }
            set
            {
                this.listView.SelectedColor = value;
            }
        }

        /// <summary>
        /// 	Border color of hovered item
        /// </summary>
        [DefaultValue(typeof(Color), "Red")]
        public Color HoveredColor
        {
            get
            {
                return this.listView.HoveredColor;
            }
            set
            {
                this.listView.HoveredColor = value;
            }
        }

        public AutocompleteMenu(FastColoredTextBox tb)
        {
            // create a new popup and add the list view to it 
            this.AutoClose = false;
            this.AutoSize = false;
            this.Margin = Padding.Empty;
            this.Padding = Padding.Empty;
            this.BackColor = Color.White;
            this.listView = new AutocompleteListView(tb);
            this.host = new ToolStripControlHost(this.listView);
            this.host.Margin = new Padding(2, 2, 2, 2);
            this.host.Padding = Padding.Empty;
            this.host.AutoSize = false;
            this.host.AutoToolTip = false;
            this.CalcSize();
            base.Items.Add(this.host);
            this.listView.Parent = this;
            this.SearchPattern = @"[\w\.]";
            this.MinFragmentLength = 2;
        }

        internal new void OnOpening(CancelEventArgs args)
        {
            if (this.Opening != null)
            {
                this.Opening(this, args);
            }
        }

        public new void Close()
        {
            this.listView.toolTip.Hide(this.listView);
            base.Close();
        }

        internal void CalcSize()
        {
            this.host.Size = this.listView.Size;
            this.Size = new System.Drawing.Size(this.listView.Size.Width + 4, this.listView.Size.Height + 4);
        }

        public virtual void OnSelecting()
        {
            this.listView.OnSelecting();
        }

        public void SelectNext(int shift)
        {
            this.listView.SelectNext(shift);
        }

        internal void OnSelecting(SelectingEventArgs args)
        {
            if (this.Selecting != null)
            {
                this.Selecting(this, args);
            }
        }

        public void OnSelected(SelectedEventArgs args)
        {
            if (this.Selected != null)
            {
                this.Selected(this, args);
            }
        }

        public new AutocompleteListView Items
        {
            get
            {
                return this.listView;
            }
        }

        /// <summary>
        /// 	Shows popup menu immediately
        /// </summary>
        /// <param name="forced"> If True - MinFragmentLength will be ignored </param>
        public void Show(bool forced)
        {
            this.Items.DoAutocomplete(forced);
        }

        /// <summary>
        /// 	Minimal size of menu
        /// </summary>
        public new Size MinimumSize
        {
            get
            {
                return this.Items.MinimumSize;
            }
            set
            {
                this.Items.MinimumSize = value;
            }
        }

        /// <summary>
        /// 	Image list of menu
        /// </summary>
        public new ImageList ImageList
        {
            get
            {
                return this.Items.ImageList;
            }
            set
            {
                this.Items.ImageList = value;
            }
        }

        /// <summary>
        /// 	Tooltip duration (ms)
        /// </summary>
        public int ToolTipDuration
        {
            get
            {
                return this.Items.ToolTipDuration;
            }
            set
            {
                this.Items.ToolTipDuration = value;
            }
        }

        /// <summary>
        /// 	Tooltip
        /// </summary>
        public ToolTip ToolTip
        {
            get
            {
                return this.Items.toolTip;
            }
            set
            {
                this.Items.toolTip = value;
            }
        }
    }

    [System.ComponentModel.ToolboxItem(false)]
    public class AutocompleteListView : UserControl
    {
        public event EventHandler FocussedItemIndexChanged;

        internal List<AutocompleteItem> visibleItems;

        private IEnumerable<AutocompleteItem> sourceItems = new List<AutocompleteItem>();

        private int focussedItemIndex;

        private int hoveredItemIndex = -1;

        private readonly int itemHeight;

        private AutocompleteMenu Menu
        {
            get
            {
                return this.Parent as AutocompleteMenu;
            }
        }

        private int oldItemCount;

        private readonly FastColoredTextBox tb;

        internal ToolTip toolTip = new ToolTip();

        private readonly System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();

        internal bool AllowTabKey { get; set; }

        public ImageList ImageList { get; set; }

        internal int AppearInterval
        {
            get
            {
                return this.timer.Interval;
            }
            set
            {
                this.timer.Interval = value;
            }
        }

        internal int ToolTipDuration { get; set; }

        public Color SelectedColor { get; set; }

        public Color HoveredColor { get; set; }

        public int FocussedItemIndex
        {
            get
            {
                return this.focussedItemIndex;
            }
            set
            {
                if (this.focussedItemIndex != value)
                {
                    this.focussedItemIndex = value;
                    if (this.FocussedItemIndexChanged != null)
                    {
                        this.FocussedItemIndexChanged(this, EventArgs.Empty);
                    }
                }
            }
        }

        public AutocompleteItem FocussedItem
        {
            get
            {
                if (this.FocussedItemIndex >= 0 && this.focussedItemIndex < this.visibleItems.Count)
                {
                    return this.visibleItems[this.focussedItemIndex];
                }
                return null;
            }
            set
            {
                this.FocussedItemIndex = this.visibleItems.IndexOf(value);
            }
        }

        internal AutocompleteListView(FastColoredTextBox tb)
        {
            this.SetStyle(
                ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
            base.Font = new Font(FontFamily.GenericSansSerif, 9);
            this.visibleItems = new List<AutocompleteItem>();
            this.itemHeight = this.Font.Height + 2;
            this.VerticalScroll.SmallChange = this.itemHeight;
            this.MaximumSize = new Size(this.Size.Width, 180);
            this.toolTip.ShowAlways = false;
            this.AppearInterval = 500;
            this.timer.Tick += this.timer_Tick;
            this.SelectedColor = Color.Orange;
            this.HoveredColor = Color.Red;
            this.ToolTipDuration = 3000;

            this.tb = tb;

            tb.KeyDown += this.tb_KeyDown;
            tb.SelectionChanged += this.tb_SelectionChanged;
            tb.KeyPressed += this.tb_KeyPressed;

            Form form = tb.FindForm();
            if (form != null)
            {
                form.LocationChanged += (o, e) => this.Menu.Close();
                form.ResizeBegin += (o, e) => this.Menu.Close();
                form.FormClosing += (o, e) => this.Menu.Close();
                form.LostFocus += (o, e) => this.Menu.Close();
            }

            tb.LostFocus += (o, e) =>
                {
                    if (!this.Menu.Focused)
                    {
                        this.Menu.Close();
                    }
                };

            tb.Scroll += (o, e) => this.Menu.Close();

            this.VisibleChanged += (o, e) =>
                {
                    if (this.Visible)
                    {
                        this.DoSelectedVisible();
                    }
                };
        }

        private void tb_KeyPressed(object sender, KeyPressEventArgs e)
        {
            bool backspaceORdel = e.KeyChar == '\b' || e.KeyChar == 0xff;

            /*
            if (backspaceORdel)
                prevSelection = tb.Selection.Start;*/

            if (this.Menu.Visible && !backspaceORdel)
            {
                this.DoAutocomplete(false);
            }
            else
            {
                this.ResetTimer(this.timer);
            }
        }

        private void timer_Tick(object sender, EventArgs e)
        {
            this.timer.Stop();
            this.DoAutocomplete(false);
        }

        private void ResetTimer(System.Windows.Forms.Timer timer)
        {
            timer.Stop();
            timer.Start();
        }

        internal void DoAutocomplete()
        {
            this.DoAutocomplete(false);
        }

        internal void DoAutocomplete(bool forced)
        {
            if (!this.Menu.Enabled)
            {
                this.Menu.Close();
                return;
            }

            this.visibleItems.Clear();
            this.FocussedItemIndex = 0;
            this.VerticalScroll.Value = 0;
            //get fragment around caret
            Range fragment = this.tb.Selection.GetFragment(this.Menu.SearchPattern);
            string text = fragment.Text;
            //calc screen point for popup menu
            Point point = this.tb.PlaceToPoint(fragment.End);
            point.Offset(2, this.tb.CharHeight);
            //
            if (forced
                ||
                (text.Length >= this.Menu.MinFragmentLength && this.tb.Selection.IsEmpty
                 /*pops up only if selected range is empty*/
                 &&
                 (this.tb.Selection.Start > fragment.Start || text.Length == 0
                 /*pops up only if caret is after first letter*/)))
            {
                this.Menu.Fragment = fragment;
                bool foundSelected = false;
                //build popup menu
                foreach (var item in this.sourceItems)
                {
                    item.Parent = this.Menu;
                    CompareResult res = item.Compare(text);
                    if (res != CompareResult.Hidden)
                    {
                        this.visibleItems.Add(item);
                    }
                    if (res == CompareResult.VisibleAndSelected && !foundSelected)
                    {
                        foundSelected = true;
                        this.FocussedItemIndex = this.visibleItems.Count - 1;
                    }
                }

                if (foundSelected)
                {
                    this.AdjustScroll();
                    this.DoSelectedVisible();
                }
            }

            //show popup menu
            if (this.Count > 0)
            {
                if (!this.Menu.Visible)
                {
                    var args = new CancelEventArgs();
                    this.Menu.OnOpening(args);
                    if (!args.Cancel)
                    {
                        this.Menu.Show(this.tb, point);
                    }
                }
                else
                {
                    this.Invalidate();
                }
            }
            else
            {
                this.Menu.Close();
            }
        }

        private void tb_SelectionChanged(object sender, EventArgs e)
        {
            /*
            FastColoredTextBox tb = sender as FastColoredTextBox;
            
            if (Math.Abs(prevSelection.iChar - tb.Selection.Start.iChar) > 1 ||
                        prevSelection.iLine != tb.Selection.Start.iLine)
                Menu.Close();
            prevSelection = tb.Selection.Start;*/
            if (this.Menu.Visible)
            {
                bool needClose = false;

                if (!this.tb.Selection.IsEmpty)
                {
                    needClose = true;
                }
                else if (!this.Menu.Fragment.Contains(this.tb.Selection.Start))
                {
                    if (this.tb.Selection.Start.iLine == this.Menu.Fragment.End.iLine
                        && this.tb.Selection.Start.iChar == this.Menu.Fragment.End.iChar + 1)
                    {
                        //user press key at end of fragment
                        char c = this.tb.Selection.CharBeforeStart;
                        if (!Regex.IsMatch(c.ToString(), this.Menu.SearchPattern)) //check char
                        {
                            needClose = true;
                        }
                    }
                    else
                    {
                        needClose = true;
                    }
                }

                if (needClose)
                {
                    this.Menu.Close();
                }
            }
        }

        private void tb_KeyDown(object sender, KeyEventArgs e)
        {
            var tb = sender as FastColoredTextBox;

            if (this.Menu.Visible)
            {
                if (this.ProcessKey(e.KeyCode, e.Modifiers))
                {
                    e.Handled = true;
                }
            }

            if (!this.Menu.Visible)
            {
                if (tb.HotkeysMapping.ContainsKey(e.KeyData)
                    && tb.HotkeysMapping[e.KeyData] == FCTBAction.AutocompleteMenu)
                {
                    this.DoAutocomplete();
                    e.Handled = true;
                }
                else
                {
                    if (e.KeyCode == Keys.Escape && this.timer.Enabled)
                    {
                        this.timer.Stop();
                    }
                }
            }
        }

        private void AdjustScroll()
        {
            if (this.oldItemCount == this.visibleItems.Count)
            {
                return;
            }

            int needHeight = this.itemHeight * this.visibleItems.Count + 1;
            this.Height = Math.Min(needHeight, this.MaximumSize.Height);
            this.Menu.CalcSize();

            this.AutoScrollMinSize = new Size(0, needHeight);
            this.oldItemCount = this.visibleItems.Count;
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            this.AdjustScroll();
            int startI = this.VerticalScroll.Value / this.itemHeight - 1;
            int finishI = (this.VerticalScroll.Value + this.ClientSize.Height) / this.itemHeight + 1;
            startI = Math.Max(startI, 0);
            finishI = Math.Min(finishI, this.visibleItems.Count);
            int y = 0;
            int leftPadding = 18;
            for (int i = startI; i < finishI; i++)
            {
                y = i * this.itemHeight - this.VerticalScroll.Value;

                AutocompleteItem item = this.visibleItems[i];

                if (item.BackColor != Color.Transparent)
                {
                    using (var brush = new SolidBrush(item.BackColor)) e.Graphics.FillRectangle(brush, 1, y, this.ClientSize.Width - 1 - 1, this.itemHeight - 1);
                }

                if (this.ImageList != null && this.visibleItems[i].ImageIndex >= 0)
                {
                    e.Graphics.DrawImage(this.ImageList.Images[item.ImageIndex], 1, y);
                }

                if (i == this.FocussedItemIndex)
                {
                    using (
                        var selectedBrush = new LinearGradientBrush(
                            new Point(0, y - 3),
                            new Point(0, y + this.itemHeight),
                            Color.Transparent,
                            this.SelectedColor))
                    using (var pen = new Pen(this.SelectedColor))
                    {
                        e.Graphics.FillRectangle(
                            selectedBrush, leftPadding, y, this.ClientSize.Width - 1 - leftPadding, this.itemHeight - 1);
                        e.Graphics.DrawRectangle(
                            pen, leftPadding, y, this.ClientSize.Width - 1 - leftPadding, this.itemHeight - 1);
                    }
                }

                if (i == this.hoveredItemIndex)
                {
                    using (var pen = new Pen(this.HoveredColor))
                        e.Graphics.DrawRectangle(
                            pen, leftPadding, y, this.ClientSize.Width - 1 - leftPadding, this.itemHeight - 1);
                }

                using (var brush = new SolidBrush(item.ForeColor != Color.Transparent ? item.ForeColor : this.ForeColor)
                    ) e.Graphics.DrawString(item.ToString(), this.Font, brush, leftPadding, y);
            }
        }

        protected override void OnScroll(ScrollEventArgs se)
        {
            base.OnScroll(se);
            this.Invalidate();
        }

        protected override void OnMouseClick(MouseEventArgs e)
        {
            base.OnMouseClick(e);

            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                this.FocussedItemIndex = this.PointToItemIndex(e.Location);
                this.DoSelectedVisible();
                this.Invalidate();
            }
        }

        protected override void OnMouseDoubleClick(MouseEventArgs e)
        {
            base.OnMouseDoubleClick(e);
            this.FocussedItemIndex = this.PointToItemIndex(e.Location);
            this.Invalidate();
            this.OnSelecting();
        }

        internal virtual void OnSelecting()
        {
            if (this.FocussedItemIndex < 0 || this.FocussedItemIndex >= this.visibleItems.Count)
            {
                return;
            }
            this.tb.TextSource.Manager.BeginAutoUndoCommands();
            try
            {
                AutocompleteItem item = this.FocussedItem;
                var args = new SelectingEventArgs { Item = item, SelectedIndex = this.FocussedItemIndex };

                this.Menu.OnSelecting(args);

                if (args.Cancel)
                {
                    this.FocussedItemIndex = args.SelectedIndex;
                    this.Invalidate();
                    return;
                }

                if (!args.Handled)
                {
                    Range fragment = this.Menu.Fragment;
                    this.DoAutocomplete(item, fragment);
                }

                this.Menu.Close();
                //
                var args2 = new SelectedEventArgs { Item = item, Tb = this.Menu.Fragment.tb };
                item.OnSelected(this.Menu, args2);
                this.Menu.OnSelected(args2);
            }
            finally
            {
                this.tb.TextSource.Manager.EndAutoUndoCommands();
            }
        }

        private void DoAutocomplete(AutocompleteItem item, Range fragment)
        {
            string newText = item.GetTextForReplace();

            //replace text of fragment
            FastColoredTextBox tb = fragment.tb;

            tb.BeginAutoUndo();
            tb.TextSource.Manager.ExecuteCommand(new SelectCommand(tb.TextSource));
            if (tb.Selection.ColumnSelectionMode)
            {
                Place start = tb.Selection.Start;
                Place end = tb.Selection.End;
                start.iChar = fragment.Start.iChar;
                end.iChar = fragment.End.iChar;
                tb.Selection.Start = start;
                tb.Selection.End = end;
            }
            else
            {
                tb.Selection.Start = fragment.Start;
                tb.Selection.End = fragment.End;
            }
            tb.InsertText(newText);
            tb.TextSource.Manager.ExecuteCommand(new SelectCommand(tb.TextSource));
            tb.EndAutoUndo();
            tb.Focus();
        }

        private int PointToItemIndex(Point p)
        {
            return (p.Y + this.VerticalScroll.Value) / this.itemHeight;
        }

        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            this.ProcessKey(keyData, Keys.None);

            return base.ProcessCmdKey(ref msg, keyData);
        }

        private bool ProcessKey(Keys keyData, Keys keyModifiers)
        {
            if (keyModifiers == Keys.None)
            {
                switch (keyData)
                {
                    case Keys.Down:
                        this.SelectNext(+1);
                        return true;
                    case Keys.PageDown:
                        this.SelectNext(+10);
                        return true;
                    case Keys.Up:
                        this.SelectNext(-1);
                        return true;
                    case Keys.PageUp:
                        this.SelectNext(-10);
                        return true;
                    case Keys.Enter:
                        this.OnSelecting();
                        return true;
                    case Keys.Tab:
                        if (!this.AllowTabKey)
                        {
                            break;
                        }
                        this.OnSelecting();
                        return true;
                    case Keys.Escape:
                        this.Menu.Close();
                        return true;
                }
            }

            return false;
        }

        public void SelectNext(int shift)
        {
            this.FocussedItemIndex = Math.Max(0, Math.Min(this.FocussedItemIndex + shift, this.visibleItems.Count - 1));
            this.DoSelectedVisible();
            //
            this.Invalidate();
        }

        private void DoSelectedVisible()
        {
            if (this.FocussedItem != null)
            {
                this.SetToolTip(this.FocussedItem);
            }

            int y = this.FocussedItemIndex * this.itemHeight - this.VerticalScroll.Value;
            if (y < 0)
            {
                this.VerticalScroll.Value = this.FocussedItemIndex * this.itemHeight;
            }
            if (y > this.ClientSize.Height - this.itemHeight)
            {
                this.VerticalScroll.Value = Math.Min(
                    this.VerticalScroll.Maximum,
                    this.FocussedItemIndex * this.itemHeight - this.ClientSize.Height + this.itemHeight);
            }
            //some magic for update scrolls
            this.AutoScrollMinSize -= new Size(1, 0);
            this.AutoScrollMinSize += new Size(1, 0);
        }

        private void SetToolTip(AutocompleteItem autocompleteItem)
        {
            string title = autocompleteItem.ToolTipTitle;
            string text = autocompleteItem.ToolTipText;

            if (string.IsNullOrEmpty(title))
            {
                this.toolTip.ToolTipTitle = null;
                this.toolTip.SetToolTip(this, null);
                return;
            }

            IWin32Window window = this.Parent ?? this;
            var location = new Point((window == this ? this.Width : this.Right) + 3, 0);

            if (string.IsNullOrEmpty(text))
            {
                this.toolTip.ToolTipTitle = null;
                this.toolTip.Show(title, window, location.X, location.Y, this.ToolTipDuration);
            }
            else
            {
                this.toolTip.ToolTipTitle = title;
                this.toolTip.Show(text, window, location.X, location.Y, this.ToolTipDuration);
            }
        }

        public int Count
        {
            get
            {
                return this.visibleItems.Count;
            }
        }

        public void SetAutocompleteItems(ICollection<string> items)
        {
            var list = new List<AutocompleteItem>(items.Count);
            foreach (var item in items)
            {
                list.Add(new AutocompleteItem(item));
            }
            SetAutocompleteItems(list);
        }

        public void SetAutocompleteItems(IEnumerable<AutocompleteItem> items)
        {
            this.sourceItems = items;
        }
    }

    public class SelectingEventArgs : EventArgs
    {
        public AutocompleteItem Item { get; internal set; }

        public bool Cancel { get; set; }

        public int SelectedIndex { get; set; }

        public bool Handled { get; set; }
    }

    public class SelectedEventArgs : EventArgs
    {
        public AutocompleteItem Item { get; internal set; }

        public FastColoredTextBox Tb { get; set; }
    }
}